using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
using System.Threading;
using System.Reflection;
using System.Net;
using System.Diagnostics;
using System.Collections;
using System.Xml.Serialization;
using System.ServiceModel.Channels;
using System.ServiceModel;
using Microsoft.MSE;
using Microsoft.MSE.Catalog.Contracts;

namespace Microsoft.MSE.Catalog.Module
{
    public class SubscriptionManager : ISubscriptionManager, IExtension<ServiceHostBase>
    {

        protected string mFilterDialect = null;
        protected const string SubscriptionManagerActor = "urn:services.microsoft.com/2005-10/ServicePlatform/WSE/SubscriptionManager/";
        protected Uri mSubscriptionManagerAddress;
        protected TimeSpan mMinExpires = new TimeSpan(1, 0, 0);
        protected Dictionary<Guid, Subscription> mSubscriptions = new Dictionary<Guid, Subscription>();
        protected Dictionary<string, Guid> mSubscriptionsByAddressAndFilter = new Dictionary<string, Guid>();

        protected TimeSpan MinExpires
        {
            get { return mMinExpires; }
            set { mMinExpires = value; }
        }
        private TimeSpan mMaxExpires = new TimeSpan(7, 0, 0, 0);

        protected TimeSpan MaxExpires
        {
            get { return mMaxExpires; }
            set { mMaxExpires = value; }
        }
        public SubscriptionManager()
        {
            mSubscriptionManagerAddress = new Uri(Configurator.svcConfig.baseAddress.ToString() + "/eventingcallback");
            mFilterDialect = SC.MSEEventingFilterDialect;
        }
        public SubscriptionManager(Uri subscriptionManagerAddress)
        {
            mSubscriptionManagerAddress = subscriptionManagerAddress;
            mFilterDialect = SC.MSEEventingFilterDialect;
        }

        protected void Remove(Subscription subscription)
        {
            string key1 = subscription.notifyToAddress + subscription.filter;
            Guid key2 = mSubscriptionsByAddressAndFilter[key1];
            mSubscriptionsByAddressAndFilter.Remove(key1);
            mSubscriptions.Remove(key2);
        }


        #region IWSEEventSource Members

        private Binding CreateBindingFromID(Guid bindingID)
        {
            MSEBinding mseBinding = CatalogSvcHelper.AssembleBindingFromBindingElements(Data.Binding.GetMSEBinding(bindingID, null, null));
            return BindingHelper.DeserializeBinding(mseBinding.BindingConfig);
        }

        private TimeSpan GetExpiresInFromMessage(string expires, ref bool expirationRelative)
        {
            TimeSpan expiresIn = TimeSpan.Zero;

            if (string.IsNullOrEmpty(expires))
            {
                if (MaxExpires != null)
                {
                    expiresIn = MaxExpires;
                }
            }
            else
            {
                if (expires.StartsWith("P"))
                { //Attempt to parse duration
                    expirationRelative = true;
                    try
                    {
                        expiresIn = XmlConvert.ToTimeSpan(expires);
                    }
                    catch (Exception ex)
                    {
                        throw new WSEInvalidExpirationTimeFault(SubscriptionManagerActor, ex.Message);
                    }
                }
                else
                { //Attempt to parse absolute time
                    try
                    {
                        expiresIn = XmlConvert.ToDateTime(expires, XmlDateTimeSerializationMode.Utc) - DateTime.Now;
                    }
                    catch (Exception ex)
                    {
                        throw new WSEInvalidExpirationTimeFault(SubscriptionManagerActor, ex.Message);
                    }

                }

                if (expiresIn <= new TimeSpan(0, 0, 0, 0))
                {
                    throw new WSEInvalidExpirationTimeFault(SubscriptionManagerActor, "Expiration must be in the future");
                }

                if ((MinExpires != null) && (expiresIn <= MinExpires)) expiresIn = MinExpires;

                if ((MaxExpires != null) && (expiresIn >= MaxExpires)) expiresIn = MaxExpires;


            }
            return expiresIn;
        }

        #endregion

        #region ISubscriptionManager Members

        public Dictionary<Guid, Subscription> GetSubscriptions()
        {
            //remove expired subscriptions before returning the list
            Dictionary<Guid, Subscription>.KeyCollection keys = mSubscriptions.Keys;

            foreach (Guid key in keys)
            {
                Subscription subscription = mSubscriptions[key];
                if (subscription.expires <= DateTime.Now)
                {
                    this.Remove(subscription);
                }
            }
            return mSubscriptions;
        }

        public virtual SubscribeResponse Subscribe(Subscribe SubscribeMsg)
        {
            SubscribeResponse response = new SubscribeResponse();
            string filter = "*";
            string endToAddress = null;
            string endToAction = null;
            string endToReferenceParameters = null;
            string notifyToAddress;
            string notifyToAction = null;
            string notifyToReferenceParameters = null;
            TimeSpan expiresIn;
            DateTime expires;
            bool expirationRelative = false;

            TraceHelper.TraceInformation("Event Subscription received");

            if (SubscribeMsg.Filter != null)
            {
                if (mFilterDialect == null)
                {
                    throw new WSEFilteringNotSupportedFault(SubscriptionManagerActor, null);
                }
                else
                {
                    string requestedDialect;
                    if ((SubscribeMsg.Filter.Dialect == null) || (string.IsNullOrEmpty(SubscribeMsg.Filter.Dialect.ToString())))
                    {
                        requestedDialect = "http://www.w3.org/TR/1999/REC-xpath-19991116";
                    }
                    else
                    {
                        requestedDialect = SubscribeMsg.Filter.Dialect.ToString();
                    }
                    if (!requestedDialect.Equals(mFilterDialect))
                    {
                        throw new WSEFilteringRequestedUnavailableFault(SubscriptionManagerActor, "Supported Dialect " + mFilterDialect);
                    }
                }
                filter = SubscribeMsg.Filter.Value;
            }

            if (SubscribeMsg.EndTo != null)
            {
                if (SubscribeMsg.EndTo.Address == null)
                {
                    throw new WSEInvalidMessageFault(SubscriptionManagerActor, "EndTo specified with no Address");
                }
                if (string.IsNullOrEmpty(SubscribeMsg.EndTo.Address.ToString()))
                {
                    throw new WSEInvalidMessageFault(SubscriptionManagerActor, "EndTo Address was an empty node");
                }

                endToAddress = SubscribeMsg.EndTo.Address.ToString();
                if (string.IsNullOrEmpty(SubscribeMsg.EndTo.Action.ToString()))
                {
                    throw new WSEInvalidMessageFault(SubscriptionManagerActor, "EndTo must specify wsa:Action");
                }
                endToAction = SubscribeMsg.EndTo.Action.ToString();

                if (SubscribeMsg.EndTo.ReferenceParameters != null) endToReferenceParameters = SubscribeMsg.EndTo.ReferenceParameters;
            }

            if (SubscribeMsg.Delivery == null)
            {
                throw new WSEInvalidMessageFault(SubscriptionManagerActor, "No Delivery Method Specified");
            }

            if ((SubscribeMsg.Delivery.Mode != null) && (!SubscribeMsg.Delivery.Mode.Equals(SC.WSEPushDeliveryModeURI)))
            {
                throw new WSEDeliveryModeRequestedUnavailableFault(SubscriptionManagerActor, "Supported Delivery Mode" + SC.WSEPushDeliveryModeURI);
            }

            if (SubscribeMsg.Delivery.NotifyTo == null)
            {
                throw new WSEInvalidMessageFault(SubscriptionManagerActor, "Delivery node must contain a single element 'NotifyTo'");
            }


            if (SubscribeMsg.Delivery.NotifyTo.Address == null)
            {
                throw new WSEInvalidMessageFault(SubscriptionManagerActor, "NotifyTo specified with no Address");
            }
            if (string.IsNullOrEmpty(SubscribeMsg.Delivery.NotifyTo.Address.ToString()))
            {
                throw new WSEInvalidMessageFault(SubscriptionManagerActor, "NotifyTo Address was an empty node");
            }

            notifyToAddress = SubscribeMsg.Delivery.NotifyTo.Address.ToString();

            if (string.IsNullOrEmpty(SubscribeMsg.Delivery.NotifyTo.Action.ToString()))
            {
                throw new WSEInvalidMessageFault(SubscriptionManagerActor, "NotifyTo must specify wsa:Action");
            }
            notifyToAction = SubscribeMsg.Delivery.NotifyTo.Action.ToString();

            notifyToReferenceParameters = SubscribeMsg.Delivery.NotifyTo.ReferenceParameters;

            expiresIn = GetExpiresInFromMessage(SubscribeMsg.Expires, ref expirationRelative);

            expires = DateTime.Now + expiresIn;

            if (expirationRelative) response.Expires = XmlConvert.ToString(expiresIn);
            else response.Expires = XmlConvert.ToString(expires, XmlDateTimeSerializationMode.Utc);

            Subscription subscription = new Subscription();
            subscription.endToAddress = endToAddress;
            subscription.endToAction = endToAction;
            subscription.endToReferenceParameters = endToReferenceParameters;
            subscription.expires = expires;
            subscription.notifyToAddress = notifyToAddress;
            subscription.notifyToAction = notifyToAction;
            subscription.notifyToBinding = CreateBindingFromID(SubscribeMsg.Delivery.NotifyTo.BindingID);
            subscription.notifyToReferenceParameters = notifyToReferenceParameters;
            subscription.filter = filter;

            string key = subscription.notifyToAddress + subscription.filter;
            Guid uuid;

            if (mSubscriptionsByAddressAndFilter.ContainsKey(key))
            {
                uuid = mSubscriptionsByAddressAndFilter[key];
                mSubscriptionsByAddressAndFilter.Remove(key);
                mSubscriptions.Remove(uuid);
            }

            uuid = Guid.NewGuid();
            mSubscriptions.Add(uuid, subscription);
            mSubscriptionsByAddressAndFilter.Add(key, uuid);

            response.SubscriptionManager = new EndpointReferenceType();
            response.SubscriptionManager.Address = mSubscriptionManagerAddress;
            response.SubscriptionManager.ReferenceParameters = uuid.ToString();

            return response;

        }

        public virtual RenewResponse Renew(Renew RenewMsg, string referenceParameters)
        {
            RenewResponse response = new RenewResponse();
            TimeSpan expiresIn;
            DateTime expires;

            bool expirationRelative = false;
            Guid uuid;
            try
            {
                uuid = new Guid(referenceParameters);
            }
            catch (Exception ex)
            {
                throw new WSEUnableToRenewFault(SubscriptionManagerActor, ex.Message);
            }
            if (!mSubscriptions.ContainsKey(uuid))
            {
                string message = string.Format("Subscription with ID {0} is not registered by this subscription manager", uuid);
                throw new WSEUnableToRenewFault(SubscriptionManagerActor, message);
            }
            expiresIn = GetExpiresInFromMessage(RenewMsg.Expires, ref expirationRelative);

            expires = DateTime.Now + expiresIn;

            mSubscriptions[uuid].expires = expires;

            if (expirationRelative) response.Expires = XmlConvert.ToString(expiresIn);
            else response.Expires = XmlConvert.ToString(expires, XmlDateTimeSerializationMode.Utc);

            return response;
        }

        public virtual void Unsubscribe(Unsubscribe UnsubscribeMsg, string referenceParameters)
        {
            Guid uuid;
            TraceHelper.TraceInformation("Event Unsubscribe received");
            try
            {
                uuid = new Guid(referenceParameters);
            }
            catch (Exception ex)
            {
                throw new WSEInvalidMessageFault(SubscriptionManagerActor, ex.Message);
            }
            if (!mSubscriptions.ContainsKey(uuid))
            {
                string message = string.Format("Subscription with ID {0} is not registered by this subscription manager", uuid);
                throw new WSEInvalidMessageFault(SubscriptionManagerActor, message);
            }
            mSubscriptions.Remove(uuid);
        }

        public virtual GetStatusResponse GetStatus(GetStatus GetStatusMsg, string referenceParameters)
        {
            Guid uuid;
            GetStatusResponse response = new GetStatusResponse();
            Hashtable data = new Hashtable();
            try
            {
                uuid = new Guid(referenceParameters);
            }
            catch (Exception ex)
            {
                throw new WSEInvalidMessageFault(SubscriptionManagerActor, ex.Message);
            }
            if (!mSubscriptions.ContainsKey(uuid))
            {
                string message = string.Format("Subscription with ID {0} is not registered by this subscription manager", uuid);
                throw new WSEInvalidMessageFault(SubscriptionManagerActor, message);
            }
            response.Expires = XmlConvert.ToString(mSubscriptions[uuid].expires, XmlDateTimeSerializationMode.Utc);

            return response;
        }

        //TODO: Add Subscription End message
        #endregion

        #region IExtension<ServiceHostBase> Members

        public void Attach(ServiceHostBase owner)
        {
        }

        public void Detach(ServiceHostBase owner)
        {
        }

        #endregion

    }
}
